package ace.cmp.spring.graphql.autoconfig;

import ace.cmp.i18n.api.AceMessageSource;
import ace.cmp.spring.graphql.core.configurer.*;
import ace.cmp.spring.graphql.core.controller.GraphqlExceptionController;
import ace.cmp.spring.graphql.core.execution.AceBatchLoaderRegistry;
import ace.cmp.spring.graphql.core.execution.impl.AceBatchLoaderRegistryImpl;
import ace.cmp.spring.graphql.core.helper.TypeDefinitionRegistryHelper;
import ace.cmp.spring.graphql.core.helper.impl.TypeDefinitionRegistryHelperImpl;
import ace.cmp.spring.graphql.core.properties.GraphqlProperties;
import graphql.execution.preparsed.PreparsedDocumentProvider;
import graphql.execution.preparsed.persisted.ApolloPersistedQuerySupport;
import graphql.execution.preparsed.persisted.InMemoryPersistedQueryCache;
import graphql.scalars.ExtendedScalars;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.data.method.annotation.support.ExtendAnnotatedControllerConfigurer;
import org.springframework.graphql.data.method.annotation.support.NullAnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;

/**
 * @author caspar
 * @date 2023/2/14 11:46
 */
@Slf4j
@EnableConfigurationProperties({GraphqlProperties.class})
@AutoConfigureBefore({GraphQlAutoConfiguration.class})
@Configuration
public class GraphqlAutoConfig {
  private final ListableBeanFactory beanFactory;

  public GraphqlAutoConfig(ListableBeanFactory beanFactory) {
    this.beanFactory = beanFactory;
  }

  @Bean
  public RuntimeWiringConfigurer runtimeWiringConfigurer() {
    return wiringBuilder ->
        wiringBuilder
            .scalar(ExtendedScalars.GraphQLLong)
            .scalar(ExtendedScalars.GraphQLBigDecimal)
            .scalar(ExtendedScalars.GraphQLByte)
            .scalar(ExtendedScalars.GraphQLBigInteger)
            .scalar(ExtendedScalars.GraphQLChar)
            .scalar(ExtendedScalars.GraphQLShort)
            .scalar(ExtendedScalars.UUID)
            .scalar(ExtendedScalars.Json)
            .scalar(ExtendedScalars.Object)
            .scalar(ExtendedScalars.DateTime)
            .scalar(ExtendedScalars.Time)
            .scalar(ExtendedScalars.Date);
  }

  @ConditionalOnProperty(
      havingValue = "true",
      value = "ace.graphql.enable-global-exception-controller",
      matchIfMissing = true)
  @ConditionalOnMissingBean
  @Bean
  public GraphqlExceptionController graphqlExceptionController(AceMessageSource messageSource) {
    return new GraphqlExceptionController(messageSource);
  }

  @Bean
  public GraphQlSourceBuilderCustomizer preparsedDocumentProviderCustomizer() {
    return builder -> {
      builder.configureGraphQl(
          graphqlBuilder -> {
            PreparsedDocumentProvider provider =
                new ApolloPersistedQuerySupport(
                    InMemoryPersistedQueryCache.newInMemoryPersistedQueryCache().build());
            graphqlBuilder.preparsedDocumentProvider(provider);
          });
    };
  }

  @Bean
  @ConditionalOnMissingBean
  public BatchLoaderRegistry batchLoaderRegistry(AceBatchLoaderRegistry aceBatchLoaderRegistry) {
    return aceBatchLoaderRegistry;
  }

  @Bean
  @ConditionalOnMissingBean
  public AceBatchLoaderRegistry aceBatchLoaderRegistry() {
    return new AceBatchLoaderRegistryImpl();
  }

  @Bean
  public GraphQlSourceBuilderCustomizer graphQlSourceBuilderAutoInjectObjectCustomizer(
      List<GraphqlSchemaInjectObjectTypeDefinition> injectObjectTypeDefinitions,
      List<GraphqlSchemaInjectToTypeField> injectToTypeFields) {
    TypeDefinitionRegistryHelper registryHelper = new TypeDefinitionRegistryHelperImpl();
    GraphqlSchemaInjectObjectTypeDefConfigurer graphqlSchemaInjectObjectTypeDefConfigurer =
        new GraphqlSchemaInjectObjectTypeDefConfigurer(injectObjectTypeDefinitions, registryHelper);
    GraphqlSchemaInjectToTypeFieldConfigurer graphqlSchemaInjectToTypeFieldConfigurer =
        new GraphqlSchemaInjectToTypeFieldConfigurer(injectToTypeFields, registryHelper);

    GraphQlSourceBuilderCustomizer customizer =
        builder -> {
          builder.configureTypeDefinitions(graphqlSchemaInjectObjectTypeDefConfigurer);
          builder.configureTypeDefinitions(graphqlSchemaInjectToTypeFieldConfigurer);
        };
    return customizer;
  }

  @Bean
  @ConditionalOnMissingBean
  public AnnotatedControllerConfigurer annotatedControllerConfigurer() {
    AnnotatedControllerConfigurer controllerConfigurer = new NullAnnotatedControllerConfigurer();
    return controllerConfigurer;
  }

  @Bean
  @ConditionalOnMissingBean
  public ExtendAnnotatedControllerConfigurer extendAnnotatedControllerConfigurer() {
    ExtendAnnotatedControllerConfigurer controllerConfigurer =
        new ExtendAnnotatedControllerConfigurer();
    controllerConfigurer.addFormatterRegistrar(
        (registry) -> ApplicationConversionService.addBeans(registry, this.beanFactory));
    return controllerConfigurer;
  }

  @Bean
  public GraphQlSourceBuilderCustomizer schemaLocationsCustomizer(
      ResourcePatternResolver resolver,
      GraphqlProperties aceGraphqlProperties,
      GraphQlProperties graphQlProperties) {
    return builder -> {
      if (CollectionUtils.isNotEmpty(aceGraphqlProperties.getSchemaLocations())) {
        Resource[] resources =
            this.resolveSchemaResources(
                resolver,
                aceGraphqlProperties.getSchemaLocations().toArray(new String[0]),
                graphQlProperties.getSchema().getFileExtensions());
        builder.schemaResources(resources);
      }
    };
  }

  private Resource[] resolveSchemaResources(
      ResourcePatternResolver resolver, String[] locations, String[] extensions) {
    List<Resource> resources = new ArrayList<>();
    for (String location : locations) {
      for (String extension : extensions) {
        resources.addAll(resolveSchemaResources(resolver, location + "*" + extension));
      }
    }
    return resources.toArray(new Resource[0]);
  }

  private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String pattern) {
    try {
      return Arrays.asList(resolver.getResources(pattern));
    } catch (IOException ex) {
      log.debug(String.format("Could not resolve schema location: '%s'", pattern), ex);
      return Collections.emptyList();
    }
  }
}
